/**
 * (created at 2011-1-25)
 */
package qiushuo.mysqlparser.recognizer.parser;

import static qiushuo.mysqlparser.recognizer.SQLTokenType.IDENTIFIER;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_BY;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_DELETE;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_LIMIT;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_WHERE;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.LITERAL_NUMBER;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.SC_QUESTION;

import java.sql.SQLSyntaxErrorException;
import java.util.LinkedList;
import java.util.List;

import qiushuo.mysqlparser.ast.expression.Expression;
import qiushuo.mysqlparser.ast.expression.primary.Identifier;
import qiushuo.mysqlparser.ast.expression.primary.ParamMarker;
import qiushuo.mysqlparser.ast.fragment.Limit;
import qiushuo.mysqlparser.ast.fragment.OrderBy;
import qiushuo.mysqlparser.ast.fragment.tableref.TableReferences;
import qiushuo.mysqlparser.ast.statement.DMLDeleteStatement;
import qiushuo.mysqlparser.recognizer.SQLToken;
import qiushuo.mysqlparser.recognizer.TokenReader;

/**
 * @author <a href="mailto:QiuShuo1985@gmail.com">QIU Shuo</a>
 */
public class DMLDeleteStmtParser extends DMLParser {
    /**
     * state of field {@link #input} should never be changed during construction
     */
    public DMLDeleteStmtParser(TokenReader input) {
        super(input);
    }

    private static final int ALT_DELETE_FROM = 1;
    private static final int ALT_DELETE_FROM_IDS_USING = 2;
    private static final int ALT_DELETE_FROM_ID_WHERE = 3; // single table
    private static final int ALT_DELETE_FROM_ID_ORDER = 4; // single table
    private static final int ALT_DELETE_FROM_ID_LIMIT = 5; // single table
    private static final int ALT_DELETE_IDS_FROM = 6;

    /**
     * nothing has been pre-consumed
     * 
     * <pre>
     * <code>'DELETE' 'LOW_PRIORITY'? 'QUICK'? 'IGNORE'? (
     *     'FROM' tid ( (',' tid)* 'USING' table_refs ('WHERE' cond)?  
     *                | ('WHERE' cond)? ('ORDER' 'BY' ids)? ('LIMIT' count)?  )  // single table
     *    | tid (',' tid)* 'FROM' table_refs ('WHERE' cond)? )</code>
     * </pre>
     */
    public DMLDeleteStatement deleteStmt() throws SQLSyntaxErrorException {
        match(KW_DELETE);
        boolean lowPriority = false;
        boolean quick = false;
        boolean ignore = false;
        int alt = 0;
        SQLToken token;
        loop1: while (true) {
            token = input.next();
            if (token == null) {
                throw err("unexpected EOF after DELETE");
            }
            switch (token.getType()) {
            case KW_LOW_PRIORITY:
                if (lowPriority) throw err("duplicated LOW_PRIORITY in delete stmt");
                lowPriority = true;
                continue;
            case KW_IGNORE:
                if (ignore) throw err("duplicated IGNORE in delete stmt");
                ignore = true;
                continue;
            case IDENTIFIER:
                if ("QUICK".equals(token.getText().toUpperCase())) {
                    if (quick) throw err("duplicated QUICK in delete stmt");
                    quick = true;
                    continue;
                }
                input.backForce();
                alt = ALT_DELETE_IDS_FROM;
                break loop1;
            case KW_FROM:
                alt = ALT_DELETE_FROM;
                break loop1;
            default:
                err("unrecognized token after DELETE");
            }
        }
        /** after <code>DELETE (options)?</code> */
        List<Identifier> tableNameList;
        Identifier tableName;
        switch (alt) {
        case ALT_DELETE_IDS_FROM:
            tableNameList = new LinkedList<Identifier>();
            if (readMultipleTableNames(tableNameList) != ALT_DELETE_IDS_FROM) {
                err("expected FROM after tableNames for delete stmt");
            }
            return tableRefsWhere(lowPriority, quick, ignore, tableNameList);
        case ALT_DELETE_FROM:
            token = match(IDENTIFIER);
            tableName = new Identifier(token.getText());
            token = input.next();
            if (token == null) {
                return new DMLDeleteStatement(lowPriority, quick, ignore, tableName);
            }
            switch (token.getType()) {
            case PUNC_COMMA:
                tableNameList = new LinkedList<Identifier>();
                tableNameList.add(tableName);
                if (readMultipleTableNames(tableNameList) != ALT_DELETE_FROM_IDS_USING) {
                    err("expected USING after tableNames for delete stmt");
                }
                return tableRefsWhere(lowPriority, quick, ignore, tableNameList);
            case KW_USING:
                tableNameList = new LinkedList<Identifier>();
                tableNameList.add(tableName);
                return tableRefsWhere(lowPriority, quick, ignore, tableNameList);
            case KW_WHERE:
                alt = ALT_DELETE_FROM_ID_WHERE;
                break;
            case KW_ORDER:
                match(KW_BY);
                alt = ALT_DELETE_FROM_ID_ORDER;
                break;
            case KW_LIMIT:
                alt = ALT_DELETE_FROM_ID_LIMIT;
                break;
            default:
                input.backForce();
                return new DMLDeleteStatement(lowPriority, quick, ignore, tableName);
            }
            break;
        default:
            throw err("unknown alt for delete stmt: " + alt);
        }
        /** after <code>WHERE</code> for single-row delete */
        Limit limit = null;
        switch (alt) {
        case ALT_DELETE_FROM_ID_WHERE:
            Expression whereCond = new ExpressionParser(input).expression();
            token = input.next();
            if (token == null) {
                return new DMLDeleteStatement(lowPriority, quick, ignore, tableName, whereCond);
            }
            switch (token.getType()) {
            case KW_ORDER:
                match(KW_BY);
                return orderLimit(lowPriority, quick, ignore, tableName, whereCond);
            case KW_LIMIT:
                if (input.isNext(LITERAL_NUMBER, 0)) {
                    token = input.next();
                    limit = new Limit("0", token.getText());
                } else if (input.isNext(SC_QUESTION, 0)) {
                    input.skipForce(1);
                    limit = new Limit("0", new ParamMarker());
                } else {
                    throw err("only LITERAL_NUMBER or ? is permitted after LIMIT");
                }
                return new DMLDeleteStatement(lowPriority, quick, ignore, tableName, whereCond, null, limit);
            default:
                input.backForce();
                return new DMLDeleteStatement(lowPriority, quick, ignore, tableName, whereCond);
            }
        case ALT_DELETE_FROM_ID_ORDER:
            return orderLimit(lowPriority, quick, ignore, tableName, null);
        case ALT_DELETE_FROM_ID_LIMIT:
            if (input.isNext(LITERAL_NUMBER, 0)) {
                token = input.next();
                limit = new Limit("0", token.getText());
            } else if (input.isNext(SC_QUESTION, 0)) {
                input.skipForce(1);
                limit = new Limit("0", new ParamMarker());
            } else {
                throw err("only LITERAL_NUMBER or ? is permitted after LIMIT");
            }
            return new DMLDeleteStatement(lowPriority, quick, ignore, tableName, null, null, limit);
        default:
            throw err("unknown alt for delete stmt: " + alt);
        }
    }

    /**
     * consume next token after tableNames when invocation finished if next
     * token is <code>USING</code> or <code>FROM</code>.<br/>
     * at least one table name
     * 
     * @return alt
     */
    private int readMultipleTableNames(List<Identifier> tableNames) throws SQLSyntaxErrorException {
        while (true) {
            SQLToken token = match(IDENTIFIER);
            tableNames.add(new Identifier(token.getText()));
            token = input.next();
            if (token == null) {
                if (tableNames.size() == 1) {
                    return ALT_DELETE_FROM_ID_WHERE;
                } else {
                    throw err("unexpected EOF for table name list");
                }
            }
            switch (token.getType()) {
            case PUNC_COMMA:
                continue;
            case KW_USING:
                return ALT_DELETE_FROM_IDS_USING;
            case KW_FROM:
                return ALT_DELETE_IDS_FROM;
            default:
                if (tableNames.size() == 1) {
                    input.backForce();
                    return ALT_DELETE_FROM_ID_WHERE;
                } else {
                    throw err("unexpected token for table name list: " + token);
                }
            }
        }
    }

    /**
     * <code>ORDER BY</code> has been consumed
     */
    private DMLDeleteStatement orderLimit(boolean lowPriority,
                                          boolean quick,
                                          boolean ignore,
                                          Identifier id,
                                          Expression whereCond) throws SQLSyntaxErrorException {
        OrderBy orderBy = orderBy();
        SQLToken token = input.next();
        if (token == null) {
            return new DMLDeleteStatement(lowPriority, quick, ignore, id, whereCond, orderBy, null);
        }
        Limit limit = null;
        if (KW_LIMIT.equals(token.getType())) {
            if (input.isNext(LITERAL_NUMBER, 0)) {
                token = input.next();
                limit = new Limit("0", token.getText());
            } else if (input.isNext(SC_QUESTION, 0)) {
                input.skipForce(1);
                limit = new Limit("0", new ParamMarker());
            } else {
                throw err("only LITERAL_NUMBER or ? is permitted after LIMIT");
            }
            return new DMLDeleteStatement(lowPriority, quick, ignore, id, whereCond, orderBy, limit);
        }
        input.backForce();
        return new DMLDeleteStatement(lowPriority, quick, ignore, id, whereCond, orderBy, null);
    }

    private DMLDeleteStatement tableRefsWhere(boolean lowPriority,
                                              boolean quick,
                                              boolean ignore,
                                              List<Identifier> tableNames) throws SQLSyntaxErrorException {
        TableReferences tableRefs = tableRefs();
        SQLToken token = input.next();
        if (token == null) {
            return new DMLDeleteStatement(lowPriority, quick, ignore, tableNames, tableRefs);
        }
        if (KW_WHERE.equals(token.getType())) {
            Expression where = new ExpressionParser(input).expression();
            return new DMLDeleteStatement(lowPriority, quick, ignore, tableNames, tableRefs, where);
        }
        input.backForce();
        return new DMLDeleteStatement(lowPriority, quick, ignore, tableNames, tableRefs);
    }

}
